package opengl.texture;

import static org.lwjgl.opengl.GL11.GL_LINEAR;
import static org.lwjgl.opengl.GL11.GL_LINEAR_MIPMAP_LINEAR;
import static org.lwjgl.opengl.GL11.GL_RGB;
import static org.lwjgl.opengl.GL11.GL_RGBA;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MAG_FILTER;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MIN_FILTER;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_S;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_T;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_BYTE;
import static org.lwjgl.opengl.GL11.glTexParameteri;
import static org.lwjgl.opengl.GL12.GL_CLAMP_TO_EDGE;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.HashMap;
import java.util.Iterator;

import log.Logger;
import opengl.renderer.GLConst;
import opengl.renderer.GLRenderer;
import opengl.renderer.RendererFactory;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.EXTTextureFilterAnisotropic;
import org.lwjgl.opengl.GL11;
import org.lwjgl.util.glu.MipMap;

import util.ResourceLoader;

public class TextureLoader{
	public static Texture getTexture(String format, InputStream in) throws IOException {
		return getTexture(format, in, false, GL11.GL_LINEAR);
	}

	public static Texture getTexture(String format, InputStream in, boolean flipped)  throws IOException {
		return getTexture(format, in, flipped, GL11.GL_LINEAR);
	}

	public static Texture getTexture(String format, InputStream in, int filter) throws IOException {
		return getTexture(format, in, false, filter);
	}

	public static Texture getTexture(String format, InputStream in, boolean flipped, int filter) throws IOException {
		return loader.getTexture(in, in.toString()+"."+format, flipped, filter,null);
	}
	
    public static Texture getTexture(String resourceName) {
    	return loader.getTexture(resourceName, false);
    }
	
	/** The renderer to use for all GL operations */
	protected static GLRenderer GL = RendererFactory.getGLRendererImmediate();
	
	private static final TextureLoader loader = new TextureLoader();
	public static TextureLoader get() {
		return loader;
	}

    private boolean deferred;
    private boolean holdTextureData;
    private boolean useMipmaps=true;
   
    private HashMap<String, Object> texturesLinear = new HashMap<String, Object>();
   
    private HashMap<String, Object> texturesNearest = new HashMap<String, Object>();
    private int dstPixelFormat = GLConst.GL_RGBA8;
	
    public Texture getTexture(String resourceName, boolean flipped, int filter, int[] transparent) throws IOException {
    	InputStream in = ResourceLoader.getResourceAsStream(resourceName);    	
    	return getTexture(in, resourceName, flipped, filter, transparent);
    }

    public Texture getTexture(String resourceName, boolean flipped) {
		try {
	    	InputStream in = ResourceLoader.getResourceAsStream(resourceName);
	    	return getTexture(in, resourceName, flipped,  GL11.GL_LINEAR, null);
		} catch (IOException e) {
			Logger.error(e, "");
		}
		return null;
    }

    
    public Texture getTexture(String resourceName, boolean flipped, int filter) throws IOException {
    	InputStream in = ResourceLoader.getResourceAsStream(resourceName);
    	return getTexture(in, resourceName, flipped, filter, null);
    }
    
    
    public Texture getTexture(File source, boolean flipped,int filter, int[] transparent) throws IOException {
    	String resourceName = source.getAbsolutePath();
    	InputStream in = new FileInputStream(source);
    	
    	return getTexture(in, resourceName, flipped, filter, transparent);
    }
    
    public TextureImpl getTexture(InputStream in, String resourceName, boolean flipped, int filter, int[] transparent) throws IOException {
    	if (deferred) {
	    	return new DeferredTexture(in, resourceName, flipped, filter, transparent);
	    }
    	
    	HashMap<String, Object> hash = texturesLinear;
        if (filter == GLConst.GL_NEAREST) {
        	hash = texturesNearest;
        }
        
        String resName = resourceName;
        if (transparent != null) {
        	resName += ":"+transparent[0]+":"+transparent[1]+":"+transparent[2];
        }
        resName += ":"+flipped;
        
        if (holdTextureData) {
        	TextureImpl tex = (TextureImpl)  hash.get(resName);
        	if (tex != null) {
        		return tex;
        	}
        } else {
	    	SoftReference<Texture> ref = (SoftReference<Texture>) hash.get(resName);
	    	if (ref != null) {
		    	TextureImpl tex = (TextureImpl) ref.get();
		        if (tex != null) {
		        	return tex;
		        } else {
		        	hash.remove(resName);
		        }
	    	}
        }
        
        // horrible test until I can find something more suitable
        try {
        	GL.glGetError();
        } catch (NullPointerException e) {
        	throw new RuntimeException("Image based resources must be loaded as part of init() or the game loop. They cannot be loaded before initialisation.");
        }
        
		TextureImpl tex = getTexture(in, resourceName, GLConst.GL_TEXTURE_2D, filter, filter, flipped, transparent);
        
        tex.setCacheName(resName);
        if (holdTextureData) {
        	hash.put(resName, tex);
        } else {
        	hash.put(resName, new SoftReference(tex));
        }
        
    	if (useMipmaps) {
    		createMipmaps(tex);
    	}
        return tex;
    }
    
	private TextureImpl getTexture(InputStream in, String resourceName, int target, int magFilter, int minFilter, boolean flipped, int[] transparent)
			throws IOException {
		// create the texture ID for this texture
		ByteBuffer textureBuffer;

		LoadableImageData imageData = new PNGImageData();
		textureBuffer = imageData.loadImage(new BufferedInputStream(in), flipped, transparent);

		int textureID = createTextureID();
		TextureImpl texture = new TextureImpl(resourceName, target, textureID);
		// bind this texture
		GL.glBindTexture(target, textureID);

		int width;
		int height;
		int texWidth;
		int texHeight;

		boolean hasAlpha;

		width = imageData.getWidth();
		height = imageData.getHeight();
		hasAlpha = imageData.getDepth() == 32;

		texture.setTextureWidth(imageData.getTexWidth());
		texture.setTextureHeight(imageData.getTexHeight());

		texWidth = texture.getTextureWidth();
		texHeight = texture.getTextureHeight();

		IntBuffer temp = BufferUtils.createIntBuffer(16);
		GL.glGetInteger(GLConst.GL_MAX_TEXTURE_SIZE, temp);
		int max = temp.get(0);
		if ((texWidth > max) || (texHeight > max)) {
			throw new IOException("Attempt to allocate a texture to big for the current hardware");
		}

		int srcPixelFormat = hasAlpha ? GLConst.GL_RGBA : GLConst.GL_RGB;
		int componentCount = hasAlpha ? 4 : 3;

		texture.setWidth(width);
		texture.setHeight(height);
		texture.setAlpha(hasAlpha);

		if (holdTextureData) {
			texture.setTextureData(srcPixelFormat, componentCount, minFilter, magFilter, textureBuffer);
		}

		GL.glTexParameteri(target, GLConst.GL_TEXTURE_MIN_FILTER, minFilter);
		GL.glTexParameteri(target, GLConst.GL_TEXTURE_MAG_FILTER, magFilter);

		// produce a texture from the byte buffer
		GL.glTexImage2D(target, 0, dstPixelFormat, get2Fold(width), get2Fold(height), 0, srcPixelFormat, GLConst.GL_UNSIGNED_BYTE, textureBuffer);

		return texture;
	}

	   public Texture createTexture(final int width, final int height) throws IOException {
	    	return createTexture(width, height, GLConst.GL_NEAREST);
	    }
	    
	    /**
	     * Create an empty texture
	     * 
	     * @param width The width of the new texture
	     * @param height The height of the new texture
	     * @return The created empty texture
	     * @throws IOException Indicates a failure to create the texture on the graphics hardware
	     */
	    public Texture createTexture(final int width, final int height, final int filter) throws IOException {
	    	ImageData ds = new EmptyImageData(width, height);
	    	
	    	return getTexture(ds, filter);
	    }
	

    public static int createTextureID() 
    { 
       IntBuffer tmp = createIntBuffer(1); 
       GL.glGenTextures(tmp); 
       return tmp.get(0);
    } 
    
    /**
     * Get a texture from a image file
     * 
     * @param dataSource The image data to generate the texture from
     * @param filter The filter to use when scaling the texture
     * @return The texture created
     * @throws IOException Indicates the texture is too big for the hardware
     */
    public Texture getTexture(ImageData dataSource, int filter) throws IOException
    { 
    	int target = GLConst.GL_TEXTURE_2D;

        ByteBuffer textureBuffer;
    	textureBuffer = dataSource.getImageBufferData();
    	
        // create the texture ID for this texture 
        int textureID = createTextureID(); 
        TextureImpl texture = new TextureImpl("generated:"+dataSource, target ,textureID); 
        
        int minFilter = filter;
        int magFilter = filter;
        boolean flipped = false;
        
        // bind this texture 
        GL.glBindTexture(target, textureID); 
    	
        int width;
        int height;
        int texWidth;
        int texHeight;
        
        boolean hasAlpha;
    	
    	width = dataSource.getWidth();
    	height = dataSource.getHeight();
    	hasAlpha = dataSource.getDepth() == 32;
    	
    	texture.setTextureWidth(dataSource.getTexWidth());
    	texture.setTextureHeight(dataSource.getTexHeight());

        texWidth = texture.getTextureWidth();
        texHeight = texture.getTextureHeight();
        
        int srcPixelFormat = hasAlpha ? GLConst.GL_RGBA : GLConst.GL_RGB;
        int componentCount = hasAlpha ? 4 : 3;
        
        texture.setWidth(width);
        texture.setHeight(height);
        texture.setAlpha(hasAlpha);
        
        IntBuffer temp = BufferUtils.createIntBuffer(16);
        GL.glGetInteger(GLConst.GL_MAX_TEXTURE_SIZE, temp);
        int max = temp.get(0);
        if ((texWidth > max) || (texHeight > max)) {
        	throw new IOException("Attempt to allocate a texture to big for the current hardware");
        }

        if (holdTextureData) {
        	texture.setTextureData(srcPixelFormat, componentCount, minFilter, magFilter, textureBuffer);
        }
        
        GL.glTexParameteri(target, GLConst.GL_TEXTURE_MIN_FILTER, minFilter); 
        GL.glTexParameteri(target, GLConst.GL_TEXTURE_MAG_FILTER, magFilter); 
        
        // produce a texture from the byte buffer
        GL.glTexImage2D(target, 
                      0, 
                      dstPixelFormat, 
                      get2Fold(width), 
                      get2Fold(height), 
                      0, 
                      srcPixelFormat, 
                      GLConst.GL_UNSIGNED_BYTE, 
                      textureBuffer); 
        
        return texture; 
    } 
    
    public static IntBuffer createIntBuffer(int size) {
        ByteBuffer temp = ByteBuffer.allocateDirect(4 * size);
        temp.order(ByteOrder.nativeOrder());

        return temp.asIntBuffer();
      } 
    
    public static int get2Fold(int fold) {
        int ret = 2;
        while (ret < fold) {
            ret *= 2;
        }
        return ret;
    } 
    
    /**
     * Reload all the textures loaded in this loader
     */
    public void reload() {
    	Iterator texs = texturesLinear.values().iterator();
    	while (texs.hasNext()) {
    		((TextureImpl) texs.next()).reload();
    	}
    	texs = texturesNearest.values().iterator();
    	while (texs.hasNext()) {
    		((TextureImpl) texs.next()).reload();
    	}
    }

    /**
     * Reload a given texture blob
     * 
     * @param texture The texture being reloaded
     * @param srcPixelFormat The source pixel format
     * @param componentCount The component count
     * @param minFilter The minification filter
     * @param magFilter The magnification filter 
     * @param textureBuffer The pixel data 
     * @return The ID of the newly created texture
     */
	public int reload(TextureImpl texture, int srcPixelFormat, int componentCount,
			int minFilter, int magFilter, ByteBuffer textureBuffer) {
    	int target = GLConst.GL_TEXTURE_2D;
        int textureID = createTextureID(); 
        GL.glBindTexture(target, textureID); 
        
        GL.glTexParameteri(target, GLConst.GL_TEXTURE_MIN_FILTER, minFilter); 
        GL.glTexParameteri(target, GLConst.GL_TEXTURE_MAG_FILTER, magFilter); 
        
        // produce a texture from the byte buffer
        GL.glTexImage2D(target, 
                      0, 
                      dstPixelFormat, 
                      texture.getTextureWidth(), 
                      texture.getTextureHeight(), 
                      0, 
                      srcPixelFormat, 
                      GLConst.GL_UNSIGNED_BYTE, 
                      textureBuffer); 
        
        return textureID; 
	}
	
    /**
     * Remove a particular named image from the cache
     * 
     * @param name The name of the image to be cleared
     */
    public void clear(String name) {
    	texturesLinear.remove(name);
    	texturesNearest.remove(name);
    }
    
    /**
     * Clear out the cached textures
     */
    public void clear() {
    	texturesLinear.clear();
    	texturesNearest.clear();
    }
    
    
    /**
     * Tell the loader to produce 16 bit textures
     */
    public void set16BitMode() {
    	dstPixelFormat = GLConst.GL_RGBA16;
    }
    
    public void setDeferredLoading(boolean deferred) {
    	this.deferred = deferred;
    }
    
    /**
     * Check if we're using deferred loading
     * 
     * @return True if we're loading deferred textures
     */
    public boolean isDeferredLoading() {
    	return deferred;
    }




	private void createMipmaps(Texture texture) {
		texture.bind();

		int width = (int) texture.getImageWidth();
		int height = (int) texture.getImageHeight();

		byte[] textureBytes = texture.getTextureData();
		int components = textureBytes.length / (width * height);

		ByteBuffer textureData = ByteBuffer.allocateDirect(textureBytes.length);
		textureData.put(textureBytes);
		textureData.rewind();

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

		glTexParameteri(GL_TEXTURE_2D, EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT, 8);

		int format = components == 3 ? GL_RGB : GL_RGBA;
		MipMap.gluBuild2DMipmaps(GL_TEXTURE_2D, components, width, height, format, GL_UNSIGNED_BYTE, textureData);
	}
}	


/*public int loadPNGTexture(String filename, int textureUnit) {
	ByteBuffer buf = null;
	int tWidth = 0;
	int tHeight = 0;
	
	try {
		// Open the PNG file as an InputStream
		InputStream in = new FileInputStream(filename);
		// Link the PNG decoder to this stream
		PNGDecoder decoder = new PNGDecoder(in);

		// Get the width and height of the texture
		tWidth = decoder.getWidth();
		tHeight = decoder.getHeight();

		// Decode the PNG file in a ByteBuffer
		buf = ByteBuffer.allocateDirect(4 * decoder.getWidth() * decoder.getHeight());
		decoder.decode(buf, decoder.getWidth() * 4, Format.RGBA);
		buf.flip();

		in.close();
	} catch (IOException e) {
		e.printStackTrace();
		System.exit(-1);
	}
	
	// Create a new texture object in memory and bind it
	int texId = GL11.glGenTextures();
	GL13.glActiveTexture(textureUnit);
	GL11.glBindTexture(GL11.GL_TEXTURE_2D, texId);
	
	// All RGB bytes are aligned to each other and each component is 1 byte
	GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
	
	// Upload the texture data and generate mip maps (for scaling)
	GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, tWidth, tHeight, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buf);
	GL30.glGenerateMipmap(GL11.GL_TEXTURE_2D);

	// Setup the ST coordinate system
	GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
	GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);

	// Setup what to do when the texture has to be scaled
	GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
	GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR_MIPMAP_LINEAR);

	//this.exitOnGLError("loadPNGTexture");

	return texId;
}
*/
